package utils

import (
	"crypto/md5"
	"crypto/rand"
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"sync"
	"sync/atomic"
	"time"
)

var idbase = idinit()

type idBase struct {
	machineId       []byte
	processId       int
	objectIdCounter uint32
	lock            *sync.RWMutex
}

func idinit() idBase {
	// readRandomUint32 returns a random objectIdCounter.
	readRandomUint32 := func() uint32 {
		var b [4]byte
		_, err := io.ReadFull(rand.Reader, b[:])
		if err != nil {
			panic(fmt.Errorf("cannot read random object id: %v", err))
		}
		return uint32((uint32(b[0]) << 0) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24))
	}

	// readMachineId generates and returns a machine id.
	// If this function fails to get the hostname it will cause a runtime error.
	readMachineId := func() []byte {
		var sum [3]byte
		id := sum[:]
		hostname, err1 := os.Hostname()
		if err1 != nil {
			_, err2 := io.ReadFull(rand.Reader, id)
			if err2 != nil {
				panic(fmt.Errorf("cannot get hostname: %v; %v", err1, err2))
			}
			return id
		}
		hw := md5.New()
		hw.Write([]byte(hostname))
		copy(id, hw.Sum(nil))
		return id
	}

	base := idBase{}
	base.machineId = readMachineId()
	base.processId = os.Getpid()
	base.objectIdCounter = readRandomUint32()
	base.lock = new(sync.RWMutex)
	return base

}

// NewObjectId returns a new unique ObjectId.
func NewId() string {
	var b [12]byte
	// Timestamp, 4 bytes, big endian
	idbase.lock.Lock()
	nowtime := time.Now()
	idbase.lock.Unlock()
	binary.BigEndian.PutUint32(b[:], uint32(nowtime.Unix()))

	// Machine, first 3 bytes of md5(hostname)
	b[4] = idbase.machineId[0]
	b[5] = idbase.machineId[1]
	b[6] = idbase.machineId[2]
	// Pid, 2 bytes, specs don't specify endianness, but we use big endian.
	b[7] = byte(idbase.processId >> 8)
	b[8] = byte(idbase.processId)
	// Increment, 3 bytes, big endian
	i := atomic.AddUint32(&idbase.objectIdCounter, 1)
	b[9] = byte(i >> 16)
	b[10] = byte(i >> 8)
	b[11] = byte(i)
	//return string(b[:])
	return fmt.Sprintf(`%x`, string(b[:]))
}

type IdByte [12]byte

func NewIdToByte() IdByte {
	var b [12]byte
	// Timestamp, 4 bytes, big endian
	idbase.lock.Lock()
	nowtime := time.Now()
	idbase.lock.Unlock()
	binary.BigEndian.PutUint32(b[:], uint32(nowtime.Unix()))

	// Machine, first 3 bytes of md5(hostname)
	b[4] = idbase.machineId[0]
	b[5] = idbase.machineId[1]
	b[6] = idbase.machineId[2]
	// Pid, 2 bytes, specs don't specify endianness, but we use big endian.
	b[7] = byte(idbase.processId >> 8)
	b[8] = byte(idbase.processId)
	// Increment, 3 bytes, big endian
	i := atomic.AddUint32(&idbase.objectIdCounter, 1)
	b[9] = byte(i >> 16)
	b[10] = byte(i >> 8)
	b[11] = byte(i)
	return b
}
